<?php

/**
 * CssMin - A (simple) css minifier with benefits
 * 
 * --
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * --
 *
 * @author		Joe Scylla <joe.scylla@gmail.com>
 * @copyright	2008 - 2010 Joe Scylla <joe.scylla@gmail.com>
 * @license		http://opensource.org/licenses/mit-license.php MIT License
 * @version		2.0.1.0064 (2010-09-30)
 */
class CssMin {

    /**
     * State: Is in document
     * 
     * @var integer
     */
    const T_DOCUMENT = 1;

    /**
     * Token: Comment
     * 
     * @var integer
     */
    const T_COMMENT = 2;

    /**
     * Token: Generic at-rule
     * 
     * @var integer
     */
    const T_AT_RULE = 3;

    /**
     * Token: Start of @media block
     * 
     * @var integer
     */
    const T_AT_MEDIA_START = 4;

    /**
     * State: Is in @media block
     * 
     * @var integer
     */
    const T_AT_MEDIA = 5;

    /**
     * Token: End of @media block
     * 
     * @var integer
     */
    const T_AT_MEDIA_END = 6;

    /**
     * Token: Start of @font-face block
     * 
     * @var integer
     */
    const T_AT_FONT_FACE_START = 7;

    /**
     * State: Is in @font-face block
     * 
     * @var integer
     */
    const T_AT_FONT_FACE = 8;

    /**
     * Token: @font-face declaration
     * 
     * @var integer
     */
    const T_FONT_FACE_DECLARATION = 9;

    /**
     * Token: End of @font-face block
     * 
     * @var integer
     */
    const T_AT_FONT_FACE_END = 10;

    /**
     * Token: Start of @page block
     * 
     * @var integer
     */
    const T_AT_PAGE_START = 11;

    /**
     * State: Is in @page block
     * 
     * @var integer
     */
    const T_AT_PAGE = 12;

    /**
     * Token: @page declaration
     * 
     * @var integer
     */
    const T_PAGE_DECLARATION = 13;

    /**
     * Token: End of @page block
     * 
     * @var integer
     */
    const T_AT_PAGE_END = 14;

    /**
     * Token: Start of ruleset
     * 
     * @var integer
     */
    const T_RULESET_START = 15;

    /**
     * Token: Ruleset selectors
     * 
     * @var integer
     */
    const T_SELECTORS = 16;

    /**
     * Token: Start of declarations
     * 
     * @var integer
     */
    const T_DECLARATIONS_START = 17;

    /**
     * State: Is in declarations
     * 
     * @var integer
     */
    const T_DECLARATIONS = 18;

    /**
     * Token: Declaration
     * 
     * @var integer
     */
    const T_DECLARATION = 19;

    /**
     * Token: End of declarations
     * 
     * @var integer
     */
    const T_DECLARATIONS_END = 20;

    /**
     * Token: End of ruleset
     * 
     * @var integer
     */
    const T_RULESET_END = 21;

    /**
     * Token: Start of @variables block
     * 
     * @var integer
     */
    const T_AT_VARIABLES_START = 100;

    /**
     * State: Is in @variables block
     * 
     * @var integer
     */
    const T_AT_VARIABLES = 101;

    /**
     * Token: @variables declaration
     * 
     * @var integer
     */
    const T_VARIABLE_DECLARATION = 102;

    /**
     * Token: End of @variables block
     * 
     * @var integer
     */
    const T_AT_VARIABLES_END = 103;

    /**
     * State: Is in string
     * 
     * @var integer
     */
    const T_STRING = 254;

    /**
     * State: Is in url string property
     * 
     * @var integer
     */
    const T_STRING_URL = 255;

    /**
     * Css transformations table
     * 
     * @var array
     */
    private static $transformations = array
        (
        "border-radius" => array("-moz-border-radius", "-webkit-border-radius", "-khtml-border-radius"),
        "border-top-left-radius" => array("-moz-border-radius-topleft", "-webkit-border-top-left-radius", "-khtml-top-left-radius"),
        "border-top-right-radius" => array("-moz-border-radius-topright", "-webkit-border-top-right-radius", "-khtml-top-right-radius"),
        "border-bottom-right-radius" => array("-moz-border-radius-bottomright", "-webkit-border-bottom-right-radius", "-khtml-border-bottom-right-radius"),
        "border-bottom-left-radius" => array("-moz-border-radius-bottomleft", "-webkit-border-bottom-left-radius", "-khtml-border-bottom-left-radius"),
        "box-shadow" => array("-moz-box-shadow", "-webkit-box-shadow", "-khtml-box-shadow"),
        "opacity" => array(array("CssMin", "_tOpacity")),
        "text-shadow" => array("-moz-text-shadow", "-webkit-text-shadow", "-khtml-text-shadow"),
        "white-space" => array(array("CssMin", "_tWhiteSpacePreWrap"))
    );

    /**
     * Minifies the Css.
     * 
     * @param string $css
     * @param array $config [optional]
     * @return string
     */
    public static function minify($css, $config = array()) {
        $tokens = self::parse($css);
        $config = array_merge(array
            (
            "remove-empty-blocks" => true,
            "remove-empty-rulesets" => true,
            "remove-last-semicolons" => true,
            "convert-css3-properties" => false,
            "convert-color-values" => false,
            "compress-color-values" => false,
            "compress-unit-values" => false,
            "emulate-css3-variables" => true,
                ), $config);
        // Minification options
        $sRemoveEmptyBlocks = $config["remove-empty-blocks"];
        $sRemoveEmptyRulesets = $config["remove-empty-rulesets"];
        $sRemoveLastSemicolon = $config["remove-last-semicolons"];
        $sConvertCss3Properties = $config["convert-css3-properties"];
        $sCompressUnitValues = $config["compress-unit-values"];
        $sConvertColorValues = $config["convert-color-values"];
        $sCompressColorValues = $config["compress-color-values"];
        $sEmulateCcss3Variables = $config["emulate-css3-variables"];
        $sRemoveTokens = array(self::T_COMMENT);
        // Remove tokens
        if (!$sEmulateCcss3Variables) {
            $sRemoveTokens = array_merge($sRemoveTokens, array(self::T_AT_VARIABLES_START, self::T_VARIABLE_DECLARATION, self::T_AT_VARIABLES_END));
        }
        for ($i = 0, $l = count($tokens); $i < $l; $i++) {
            if (in_array($tokens[$i][0], $sRemoveTokens)) {
                unset($tokens[$i]);
            }
        }
        $tokens = array_values($tokens);
        // Remove empty rulesets
        if ($sRemoveEmptyRulesets) {
            for ($i = 0, $l = count($tokens); $i < $l; $i++) {
                // Remove empty rulesets
                if ($tokens[$i][0] == self::T_RULESET_START && $tokens[$i + 4][0] == self::T_RULESET_END) {
                    unset($tokens[$i]);  // T_RULESET_START
                    unset($tokens[++$i]); // T_SELECTORS
                    unset($tokens[++$i]); // T_DECLARATIONS_START
                    unset($tokens[++$i]); // T_DECLARATIONS_END
                    unset($tokens[++$i]); // T_RULESET_END
                }
            }
            $tokens = array_values($tokens);
        }
        // Remove empty @media, @font-face or @page blocks
        if ($sRemoveEmptyBlocks) {
            for ($i = 0, $l = count($tokens); $i < $l; $i++) {
                // Remove empty @media, @font-face or @page blocks
                if (($tokens[$i][0] == self::T_AT_MEDIA_START && $tokens[$i + 1][0] == self::T_AT_MEDIA_END) || ($tokens[$i][0] == self::T_AT_FONT_FACE_START && $tokens[$i + 1][0] == self::T_AT_FONT_FACE_END) || ($tokens[$i][0] == self::T_AT_PAGE_START && $tokens[$i + 1][0] == self::T_AT_PAGE_END)) {
                    unset($tokens[$i]);  // T_AT_MEDIA_START, T_AT_FONT_FACE_START, T_AT_PAGE_START
                    unset($tokens[++$i]); // T_AT_MEDIA_END, T_AT_FONT_FACE_END, T_AT_PAGE_END
                }
            }
            $tokens = array_values($tokens);
        }
        // CSS Level 3 variables: parse variables
        if ($sEmulateCcss3Variables) {
            // Parse variables
            $variables = array();
            for ($i = 0, $l = count($tokens); $i < $l; $i++) {
                if ($tokens[$i][0] == self::T_VARIABLE_DECLARATION) {
                    for ($i2 = 0, $l2 = count($tokens[$i][3]); $i2 < $l2; $i2++) {
                        if (!isset($variables[$tokens[$i][3][$i2]])) {
                            $variables[$tokens[$i][3][$i2]] = array();
                        }
                        $variables[$tokens[$i][3][$i2]][$tokens[$i][1]] = $tokens[$i][2];
                    }
                }
            }
        }
        // Conversion and compression 
        for ($i = 0, $l = count($tokens); $i < $l; $i++) {
            if ($tokens[$i][0] == self::T_DECLARATION) {
                // CSS Level 3 variables
                if ($sEmulateCcss3Variables) {
                    if (substr($tokens[$i][2], 0, 4) == "var(" && substr($tokens[$i][2], -1, 1) == ")") {
                        $tokens[$i][3][] = "all";
                        $variable = trim(substr($tokens[$i][2], 4, -1));
                        for ($i2 = 0, $l2 = count($tokens[$i][3]); $i2 < $l2; $i2++) {
                            if (isset($variables[$tokens[$i][3][$i2]][$variable])) {
                                $tokens[$i][2] = $variables[$tokens[$i][3][$i2]][$variable];
                                break;
                            }
                        }
                    }
                }
                // Compress unit values
                if ($sCompressUnitValues) {
                    // Compress "0.5px" to ".5px"
                    $tokens[$i][2] = preg_replace("/(^| |-)0\.([0-9]+)(%|em|ex|px|in|cm|mm|pt|pc)/iS", "\${1}.\${2}\${3}", $tokens[$i][2]);
                    // Compress "0px" to "0"
                    $tokens[$i][2] = preg_replace("/(^| )-?(\.?)0(%|em|ex|px|in|cm|mm|pt|pc)/iS", "\${1}0", $tokens[$i][2]);
                    // Compress "0 0 0 0" to "0"
                    if ($tokens[$i][2] == "0 0 0 0") {
                        $tokens[$i][2] = "0";
                    }
                }
                // Convert RGB color values to hex ("rgb(200,60%,5)" => "#c89905")
                if ($sConvertColorValues && preg_match("/rgb\s*\(\s*([0-9%]+)\s*,\s*([0-9%]+)\s*,\s*([0-9%]+)\s*\)/iS", $tokens[$i][2], $m)) {
                    for ($i2 = 1, $l2 = count($m); $i2 < $l2; $i2++) {
                        if (strpos("%", $m[$i2]) !== false) {
                            $m[$i2] = substr($m[$i2], 0, -1);
                            $m[$i2] = (int) (256 * ($m[$i2] / 100));
                        }
                        $m[$i2] = str_pad(dechex($m[$i2]), 2, "0", STR_PAD_LEFT);
                    }
                    $tokens[$i][2] = str_replace($m[0], "#" . $m[1] . $m[2] . $m[3], $tokens[$i][2]);
                }
                // Compress color values ("#aabbcc" to "#abc") 
                if ($sCompressColorValues && preg_match("/\#([0-9a-f]{6})/iS", $tokens[$i][2], $m)) {
                    $m[1] = strtolower($m[1]);
                    if (substr($m[1], 0, 1) == substr($m[1], 1, 1) && substr($m[1], 2, 1) == substr($m[1], 3, 1) && substr($m[1], 4, 1) == substr($m[1], 5, 1)) {
                        $tokens[$i][2] = str_replace($m[0], "#" . substr($m[1], 0, 1) . substr($m[1], 2, 1) . substr($m[1], 4, 1), $tokens[$i][2]);
                    }
                }
            }
        }
        // Create minified css
        $r = "";
        for ($i = 0, $l = count($tokens); $i < $l; $i++) {
            // T_AT_RULE
            if ($tokens[$i][0] == self::T_AT_RULE) {
                $r .= "@" . $tokens[$i][1] . " " . $tokens[$i][2] . ";";
            }
            // T_AT_MEDIA_START
            elseif ($tokens[$i][0] == self::T_AT_MEDIA_START) {
                if (count($tokens[$i][1]) == 1 && $tokens[$i][1][0] == "all") {
                    $r .= "@media{";
                } else {
                    $r .= "@media " . implode(",", $tokens[$i][1]) . "{";
                }
            }
            // T_AT_FONT_FACE_START
            elseif ($tokens[$i][0] == self::T_AT_FONT_FACE_START) {
                $r .= "@font-face{";
            }
            // T_FONT_FACE_DECLARATION
            elseif ($tokens[$i][0] == self::T_FONT_FACE_DECLARATION) {
                $r .= $tokens[$i][1] . ":" . $tokens[$i][2] . ($sRemoveLastSemicolon && $tokens[$i + 1][0] == self::T_AT_FONT_FACE_END ? "" : ";");
            }
            // T_AT_PAGE_START
            elseif ($tokens[$i][0] == self::T_AT_PAGE_START) {
                $r .= "@page{";
            }
            // T_PAGE_DECLARATION
            elseif ($tokens[$i][0] == self::T_PAGE_DECLARATION) {
                $r .= $tokens[$i][1] . ":" . $tokens[$i][2] . ($sRemoveLastSemicolon && $tokens[$i + 1][0] == self::T_AT_PAGE_END ? "" : ";");
            }
            // T_SELECTORS
            elseif ($tokens[$i][0] == self::T_SELECTORS) {
                $r .= implode(",", $tokens[$i][1]);
            }
            // Start of declarations
            elseif ($tokens[$i][0] == self::T_DECLARATIONS_START) {
                $r .= "{";
            }
            // T_DECLARATION
            elseif ($tokens[$i][0] == self::T_DECLARATION) {
                if ($sConvertCss3Properties && isset(self::$transformations[$tokens[$i][1]])) {
                    foreach (self::$transformations[$tokens[$i][1]] as $value) {
                        if (!is_array($value)) {
                            $r .= $value . ":" . $tokens[$i][2] . ";";
                        } elseif (is_array($value) && is_callable($value)) {
                            $r.= call_user_func_array($value, array($tokens[$i][1], $tokens[$i][2]));
                        }
                    }
                }
                $r .= $tokens[$i][1] . ":" . $tokens[$i][2] . ($sRemoveLastSemicolon && $tokens[$i + 1][0] == self::T_DECLARATIONS_END ? "" : ";");
            }
            // T_DECLARATIONS_END, T_AT_MEDIA_END, T_AT_FONT_FACE_END, T_AT_PAGE_END
            elseif (in_array($tokens[$i][0], array(self::T_DECLARATIONS_END, self::T_AT_MEDIA_END, self::T_AT_FONT_FACE_END, self::T_AT_PAGE_END))) {
                $r .= "}";
            } else {
                // Tokens with no output:
                // T_COMMENT
                // T_RULESET_START
                // T_RULESET_END
                // T_AT_VARIABLES_START
                // T_VARIABLE_DECLARATION
                // T_AT_VARIABLES_END
            }
        }
        return $r;
    }

    /**
     * Parses the Css and returns a array of tokens.
     * 
     * @param string $css
     * @return array
     */
    public static function parse($css) {
        // Settings
        $sDefaultScope = array("all");      // Default scope
        $sDefaultTrim = " \t\n\r\0\x0B";     // Default trim charlist
        $sTokenChars = "@{}();:\n\"'/*,";    // Tokens triggering parser processing
        // Basic variables
        $c = null;        // Current char
        $p = null;        // Previous char
        $buffer = "";        // Buffer
        $state = array(self::T_DOCUMENT);   // State stack
        $currentState = self::T_DOCUMENT;     // Current state
        $scope = $sDefaultScope;     // Current scope
        $stringChar = null;        // String delimiter char
        $isFilterWs = true;        // Filter double whitespaces?
        $selectors = array();       // Array with collected selectors
        $r = array();       // Return value
        // Prepare css
        $css = str_replace("\r\n", "\n", $css); // Windows to Unix line endings
        $css = str_replace("\r", "\n", $css); // Mac to Unix line endings
        while (strpos($css, "\n\n") !== false) {
            $css = str_replace("\n\n", "\n", $css); // Remove double line endings
        }
        $css = str_replace("\t", " ", $css);  // Convert tabs to spaces
        // Parse css 
        for ($i = 0, $l = strlen($css); $i < $l; $i++) {
            $c = substr($css, $i, 1);
            // Filter out double spaces
            if ($isFilterWs && $c == " " && $c == $p) {
                continue;
            }
            $buffer .= $c;
            if (strpos($sTokenChars, $c) !== false) {
                // 
                $currentState = $state[count($state) - 1];
                /*
                 * Start of comment
                 */
                if ($p == "/" && $c == "*" && $currentState != self::T_STRING && $currentState != self::T_COMMENT) {
                    $saveBuffer = substr($buffer, 0, -2); // save the buffer (will get restored with comment ending)
                    $buffer = $c;
                    $isFilterWs = false;
                    array_push($state, self::T_COMMENT);
                }
                /*
                 * End of comment
                 */ elseif ($p == "*" && $c == "/" && $currentState == self::T_COMMENT) {
                    $r[] = array(self::T_COMMENT, trim($buffer));
                    $buffer = $saveBuffer;
                    $isFilterWs = true;
                    array_pop($state);
                }
                /*
                 * Start of string
                 */ elseif (($c == "\"" || $c == "'") && $currentState != self::T_STRING && $currentState != self::T_COMMENT && $currentState != self::T_STRING_URL) {
                    $stringChar = $c;
                    $isFilterWs = false;
                    array_push($state, self::T_STRING);
                }
                /**
                 * Escaped LF in string => remove escape backslash and LF
                 */ elseif ($c == "\n" && $p == "\\" && $currentState == self::T_STRING) {
                    $buffer = substr($buffer, 0, -2);
                }
                /*
                 * End of string
                 */ elseif ($c === $stringChar && $currentState == self::T_STRING) {
                    if ($p == "\\") { // Previous char is a escape char
                        $count = 1;
                        $i2 = $i - 2;
                        while (substr($css, $i2, 1) == "\\") {
                            $count++;
                            $i2--;
                        }
                        // if count of escape chars is uneven => continue with string...
                        if ($count % 2) {
                            continue;
                        }
                    }
                    // ...else end the string
                    $isFilterWs = true;
                    array_pop($state);
                    $stringChar = null;
                }
                /**
                 * Start of url string property
                 */ elseif ($c == "(" && ($currentState != self::T_COMMENT && $currentState != self::T_STRING) && strtolower(substr($css, $i - 3, 3) == "url") && ($currentState == self::T_DECLARATION || $currentState == self::T_FONT_FACE_DECLARATION || $currentState == self::T_PAGE_DECLARATION || $currentState == self::T_VARIABLE_DECLARATION)) {
                    array_push($state, self::T_STRING_URL);
                }
                /**
                 * End of url string property
                 */ elseif (($c == ")" || $c == "\n") && ($currentState != self::T_COMMENT && $currentState != self::T_STRING) && $currentState == self::T_STRING_URL) {
                    if ($p == "\\") {
                        continue;
                    }
                    array_pop($state);
                }
                /*
                 * Start of at-rule @media block
                 */ elseif ($c == "@" && $currentState == self::T_DOCUMENT && strtolower(substr($css, $i, 6)) == "@media") {
                    $i = $i + 6;
                    $buffer = "";
                    array_push($state, self::T_AT_MEDIA_START);
                }
                /*
                 * At-rule @media block media types
                 */ elseif ($c == "{" && $currentState == self::T_AT_MEDIA_START) {
                    $buffer = strtolower(trim($buffer, $sDefaultTrim . "{"));
                    $scope = $buffer != "" ? array_filter(array_map("trim", explode(",", $buffer))) : $sDefaultScope;
                    $r[] = array(self::T_AT_MEDIA_START, $scope);
                    $i = $i++;
                    $buffer = "";
                    array_pop($state);
                    array_push($state, self::T_AT_MEDIA);
                }
                /*
                 * End of at-rule @media block
                 */ elseif ($currentState == self::T_AT_MEDIA && $c == "}") {
                    $r[] = array(self::T_AT_MEDIA_END);
                    $scope = $sDefaultScope;
                    $buffer = "";
                    array_pop($state);
                }
                /*
                 * Start of at-rule @font-face block
                 */ elseif ($c == "@" && $currentState == self::T_DOCUMENT && strtolower(substr($css, $i, 10)) == "@font-face") {
                    $r[] = array(self::T_AT_FONT_FACE_START);
                    $i = $i + 10;
                    $buffer = "";
                    array_push($state, self::T_AT_FONT_FACE);
                }
                /*
                 * @font-face declaration: Property
                 */ elseif ($c == ":" && $currentState == self::T_AT_FONT_FACE) {
                    $property = trim($buffer, $sDefaultTrim . ":{");
                    $buffer = "";
                    array_push($state, self::T_FONT_FACE_DECLARATION);
                }
                /*
                 * @font-face declaration: Value
                 */ elseif (($c == ";" || $c == "}" || $c == "\n") && $currentState == self::T_FONT_FACE_DECLARATION) {
                    $value = trim($buffer, $sDefaultTrim . ";}");
                    $r[] = array(self::T_FONT_FACE_DECLARATION, $property, $value, $scope);
                    $buffer = "";
                    array_pop($state);
                    if ($c == "}") { // @font-face declaration closed with a right curly brace => closes @font-face block
                        array_pop($state);
                        $r[] = array(self::T_AT_FONT_FACE_END);
                    }
                }
                /*
                 * End of at-rule @font-face block
                 */ elseif ($c == "}" && $currentState == self::T_AT_FONT_FACE) {
                    $r[] = array(self::T_AT_FONT_FACE_END);
                    $buffer = "";
                    array_pop($state);
                }
                /*
                 * Start of at-rule @page block
                 */ elseif ($c == "@" && $currentState == self::T_DOCUMENT && strtolower(substr($css, $i, 5)) == "@page") {
                    $r[] = array(self::T_AT_PAGE_START);
                    $i = $i + 5;
                    $buffer = "";
                    array_push($state, self::T_AT_PAGE);
                }
                /*
                 * @page declaration: Property
                 */ elseif ($c == ":" && $currentState == self::T_AT_PAGE) {
                    $property = trim($buffer, $sDefaultTrim . ":{");
                    $buffer = "";
                    array_push($state, self::T_PAGE_DECLARATION);
                }
                /*
                 * @page declaration: Value
                 */ elseif (($c == ";" || $c == "}" || $c == "\n") && $currentState == self::T_PAGE_DECLARATION) {
                    $value = trim($buffer, $sDefaultTrim . ";}");
                    $r[] = array(self::T_PAGE_DECLARATION, $property, $value, $scope);
                    $buffer = "";
                    array_pop($state);
                    if ($c == "}") { // @page declaration closed with a right curly brace => closes @font-face block
                        array_pop($state);
                        $r[] = array(self::T_AT_PAGE_END);
                    }
                }
                /*
                 * End of at-rule @page block
                 */ elseif ($c == "}" && $currentState == self::T_AT_PAGE) {
                    $r[] = array(self::T_AT_PAGE_END);
                    $buffer = "";
                    array_pop($state);
                }
                /*
                 * Start of at-rule @variables block
                 */ elseif ($c == "@" && $currentState == self::T_DOCUMENT && strtolower(substr($css, $i, 10)) == "@variables") {
                    $i = $i + 10;
                    $buffer = "";
                    array_push($state, self::T_AT_VARIABLES_START);
                }
                /*
                 * @variables media types
                 */ elseif ($c == "{" && $currentState == self::T_AT_VARIABLES_START) {
                    $buffer = strtolower(trim($buffer, $sDefaultTrim . "{"));
                    $r[] = array(self::T_AT_VARIABLES_START, $scope);
                    $scope = $buffer != "" ? array_filter(array_map("trim", explode(",", $buffer))) : $sDefaultScope;
                    $i = $i++;
                    $buffer = "";
                    array_pop($state);
                    array_push($state, self::T_AT_VARIABLES);
                }
                /*
                 * @variables declaration: Property
                 */ elseif ($c == ":" && $currentState == self::T_AT_VARIABLES) {
                    $property = trim($buffer, $sDefaultTrim . ":");
                    $buffer = "";
                    array_push($state, self::T_VARIABLE_DECLARATION);
                }
                /*
                 * @variables declaration: Value
                 */ elseif (($c == ";" || $c == "}" || $c == "\n") && $currentState == self::T_VARIABLE_DECLARATION) {
                    $value = trim($buffer, $sDefaultTrim . ";}");
                    $r[] = array(self::T_VARIABLE_DECLARATION, $property, $value, $scope);
                    $buffer = "";
                    array_pop($state);
                    if ($c == "}") { // @variable declaration closed with a right curly brace => closes @variables block
                        array_pop($state);
                        $r[] = array(self::T_AT_VARIABLES_END);
                        $scope = $sDefaultScope;
                    }
                }
                /*
                 * End of at-rule @variables block
                 */ elseif ($c == "}" && $currentState == self::T_AT_VARIABLES) {
                    $r[] = array(self::T_AT_VARIABLES_END);
                    $scope = $sDefaultScope;
                    $buffer = "";
                    array_pop($state);
                }
                /*
                 * Start of document level at-rule
                 */ elseif ($c == "@" && $currentState == self::T_DOCUMENT) {
                    $buffer = "";
                    array_push($state, self::T_AT_RULE);
                }
                /*
                 * End of document level at-rule
                 */ elseif ($c == ";" && $currentState == self::T_AT_RULE) {
                    $pos = strpos($buffer, " ");
                    $rule = substr($buffer, 0, $pos);
                    $value = trim(substr($buffer, $pos), $sDefaultTrim . ";");
                    $r[] = array(self::T_AT_RULE, $rule, $value);
                    $buffer = "";
                    array_pop($state);
                }
                /**
                 * Selector
                 */ elseif ($c == "," && ($currentState == self::T_AT_MEDIA || $currentState == self::T_DOCUMENT)) {
                    $selectors[] = trim($buffer, $sDefaultTrim . ",");
                    $buffer = "";
                }
                /*
                 * Start of ruleset
                 */ elseif ($c == "{" && ($currentState == self::T_AT_MEDIA || $currentState == self::T_DOCUMENT)) {
                    $selectors[] = trim($buffer, $sDefaultTrim . "{");
                    $selectors = array_filter(array_map("trim", $selectors));
                    $r[] = array(self::T_RULESET_START);
                    $r[] = array(self::T_SELECTORS, $selectors);
                    $r[] = array(self::T_DECLARATIONS_START);
                    $buffer = "";
                    $selectors = array();
                    array_push($state, self::T_DECLARATIONS);
                }
                /*
                 * Declaration: Property
                 */ elseif ($c == ":" && $currentState == self::T_DECLARATIONS) {
                    $property = trim($buffer, $sDefaultTrim . ":;");
                    $buffer = "";
                    array_push($state, self::T_DECLARATION);
                }
                /*
                 * Declaration: Value
                 */ elseif (($c == ";" || $c == "}" || $c == "\n") && $currentState == self::T_DECLARATION) {
                    $value = trim($buffer, $sDefaultTrim . ";}");
                    $r[] = array(self::T_DECLARATION, $property, $value, $scope);
                    $buffer = "";
                    array_pop($state);
                    if ($c == "}") { // declaration closed with a right curly brace => close ruleset
                        array_pop($state);
                        $r[] = array(self::T_DECLARATIONS_END);
                        $r[] = array(self::T_RULESET_END);
                    }
                }
                /*
                 * End of ruleset
                 */ elseif ($c == "}" && $currentState == self::T_DECLARATIONS) {
                    $r[] = array(self::T_DECLARATIONS_END);
                    $r[] = array(self::T_RULESET_END);
                    $buffer = "";
                    array_pop($state);
                }
            }

            $p = $c;
        }
        return $r;
    }

    /**
     * Transforms "opacity: {value}" into browser specific counterparts.
     * 
     * @param string $property
     * @param string $value
     * @return string
     */
    private static function _tOpacity($property, $value) {
        $ieValue = (int) ((float) $value * 100);
        $r = "-moz-opacity:" . $value . ";";      // Firefox < 3.5
        $r .= "-ms-filter: \"alpha(opacity=" . $ieValue . ")\";"; // Internet Explorer 8
        $r .= "filter: alpha(opacity=" . $ieValue . ");zoom: 1;"; // Internet Explorer 4 - 7
        return $r;
    }

    /**
     * Transforms "white-space: pre-wrap" into browser specific counterparts.
     * 
     * @param string $property
     * @param string $value
     * @return string
     */
    private static function _tWhiteSpacePreWrap($property, $value) {
        if (strtolower($value) == "pre-wrap") {
            $r = "white-space:-moz-pre-wrap;";  // Mozilla
            $r .= "white-space:-webkit-pre-wrap;"; // Webkit
            $r .= "white-space:-khtml-pre-wrap;"; // khtml
            $r .= "white-space:-pre-wrap;";   // Opera 4 - 6
            $r .= "white-space:-o-pre-wrap;";  // Opera 7+
            $r .= "word-wrap:break-word;";   // Internet Explorer 5.5+
            return $r;
        } else {
            return "";
        }
    }

}

?>